// 元素选择相关功能
class ElementSelector {
    // 鼠标所在的元素
    #overElement = null
    // 鼠标所在元素的初始色（鼠标未进入前的颜色）
    #overElementOriginBackgroundColor = null
    // 已经选中的元素，key:element,value=[原始背景色，原始边框样式]
    #selectedElements = new Map()
    // 预选中的元素
    #preSelectElements = new Map()
    // 记录选中的元素的xpath
    #selectedXpathMatchUnique = []
    #selectedXpathMatchSimilar = null
    // 递归获取xpath时，记录沿途节点的tagName
    #xpathTemplate = []
    // 是否允许运行预选算法
    #allowPreSelect = true
    // 测试xpath时选中的元素
    #testXpathSelectElements = new Map()
    // 选择元素时的场景。0-正常选择场景，1-修改节点属性，用鼠标获取xpath的场景
    #selectScene = 0

    constructor() {
    }

    #isUnique(xpath) {
        const result = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
        return result.snapshotLength === 1 // 唯一性验证
    }

    #getXpath(element) {
        // 反向记录xpath路径上的元素类型
        this.#xpathTemplate.push(element.tagName)
        const tagName = element.tagName.toLowerCase()
        // 递归到根节点，终止
        if (tagName === 'body') {
            return "//body"
        }
        if (element.id) {
            return `//${tagName}[@id='${element.id}']`
        }
        if (element.className) {
            const xpath = `//${tagName}[@class='${element.className}']`
            if (this.#isUnique(xpath)) {
                return xpath
            }
        }
        if (element.name) {
            const xpath = `//${tagName}[@name="${element.name}"]`
            if (this.#isUnique(xpath)) {
                return xpath
            }
        }
        // 向上递归
        let parentXpath = this.#getXpath(element.parentElement)
        // 获取当前元素在同类兄弟节点中的索引
        let index = 1
        let previousSibling = element.previousElementSibling
        while (previousSibling) {
            if (previousSibling.tagName.toLowerCase() === tagName) {
                index++
            }
            previousSibling = previousSibling.previousElementSibling
        }
        return `${parentXpath}/${tagName}[${index}]`
    }

    #getXpathMatchUnique(element) {
        const xpath = this.#getXpath(element)
        this.#selectedXpathMatchUnique.push(xpath)
        return xpath
    }

    // 将返回一个xpath，能匹配到用户选中的元素以及同类元素
    #getXpathMatchSimilar(element) {
        // 获取用户选中的那一个元素的xpath，并获取到每一层的内容
        // ej. xpath: //div[@class='test']/ul[2]/li[1]/span[2]
        //     xpathSplit: ["div[@class='test']","ul[2]","li[1]","span[2]"]
        //     xpathTemplate: ['DIV','UL','LI','SPAN']
        this.#getXpathMatchUnique(element)
        this.#xpathTemplate.reverse()
        let xpathSplit = this.#selectedXpathMatchUnique[0].split('/')
        if (xpathSplit[1] === '') {
            xpathSplit = xpathSplit.slice(2)
        } else {
            xpathSplit = xpathSplit.slice(1)
        }

        // 基于xpathSplit和xpathTemplate组装出能匹配多个同类元素的xpath
        // 规则为：取通过特殊属性(id,class,name)定位的前缀，这部分保持不变，之后的部分去除索引部分
        let xpathMathMore = ['/']
        for (let i = 0; i < xpathSplit.length; i++) {
            xpathMathMore.push('/')
            if (xpathSplit[i].includes('@')) {
                xpathMathMore.push(xpathSplit[i])
            } else {
                xpathMathMore.push(this.#xpathTemplate[i].toLowerCase())
            }
        }

        return this.#selectedXpathMatchSimilar = xpathMathMore.join('')
    }

    mouseOver(event) {
        const target = event.target
        // 恢复之前高亮元素的背景色，这种情况是鼠标进入一个元素的子元素时，恢复该元素的父元素的背景色
        if (this.#overElement) {
            this.#overElement = this.#overElementOriginBackgroundColor
        }
        // 记录当前元素及其背景色
        this.#overElement = target;
        this.#overElementOriginBackgroundColor = target.style.backgroundColor
        // 修改当前目标元素的背景色
        target.style.backgroundColor = 'rgba(135, 206, 250, 0.5)';
    }

    mouseOut(event) {
        const target = event.target
        if (this.#overElement !== target) {
            return
        }
        target.style.backgroundColor = this.#overElementOriginBackgroundColor
        this.#overElement = null
        this.#overElementOriginBackgroundColor = null
    }

    mouseSelect(event) {
        event.preventDefault();
        // 如果此时有测试xpath的元素选中，先清除
        if (this.#testXpathSelectElements.size !== 0) {
            this.#clearXpathTestSelect()
        }

        let element = event.target
        if (this.#selectScene === 1) {
            // 当处于修改节点属性的场景时，鼠标点击的元素就是用户想要获取xpath的元素
            this.#getXpath(element)
            window.embeddedPageEvent.returnSelectXpath(this.#getXpath(element))
            return
        }

        // 再次选中已选中元素，不会发生任何事
        if (this.#selectedElements.has(element)) {
            return
        }
        // 在被预选的元素中用户进行了选中，我们认为用户不想使用预选。当进行下轮选择时(此轮选择被取消或采用)，预选才将被再次开启
        if (this.#preSelectElements.has(element)) {
            this.#selectedXpathMatchSimilar = null
            this.#selectedXpathMatchUnique.push(this.#getXpath(element))
            this.#select(element)
            this.#selectedElements.set(element, this.#preSelectElements.get(element))
            // 方便鼠标移出该元素时，恢复其为被选中的样式
            this.#overElementOriginBackgroundColor = 'rgba(13,142,4,0.3)'

            this.#preSelectElements.delete(element)
            // 其余预选元素被还原
            this.#preSelectElements.forEach((v, k, m) => this.#cancelSelect(k, v))
            this.#preSelectElements.clear()
            // 关闭预选
            this.#allowPreSelect = false
            return
        }

        // 用户选择了一个新的元素，这要分三种情况
        // 1.预选算法选中了0个元素，此时不会在运行预选算法
        // 2.用户应用了上一次的预选结果，此时不会运行预选算法。“选择全部”的回调会修改allowPreSelect
        // 3.用户放弃了上一次的预选结果，此时会重新运行预选算法。相当于进行了新一轮的选择
        if (this.#allowPreSelect && (this.#preSelectElements.size > 1 || this.#selectedXpathMatchUnique.length === 0)) {
            this.reset()
            this.#selectedElements.set(element, [this.#overElementOriginBackgroundColor, element.style.border])
            this.#select(element)
            // 获取到一个通用xpath，匹配到所有相似元素
            const xp = this.#getXpathMatchSimilar(element)
            const allMatchElements = document.evaluate(xp, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
            // 遍历所有相似元素，将其设置为预选中的样式
            for (let i = 0; i < allMatchElements.snapshotLength; i++) {
                const e = allMatchElements.snapshotItem(i)
                if (e !== element) {
                    this.#preSelectElements.set(e, [e.style.backgroundColor, e.style.border])
                    this.#preSelect(e)
                }
            }
            // 没有预选到同类元素
            if (this.#preSelectElements.size === 0) {
                this.#selectedXpathMatchSimilar = null
            }
        } else {
            if (this.#allowPreSelect) {
                this.#selectedXpathMatchSimilar = null
            }
            this.#allowPreSelect = false
            this.#getXpathMatchUnique(element)
            this.#selectedElements.set(element, [this.#overElementOriginBackgroundColor, element.style.border])
            this.#select(element)
        }
        this.#overElementOriginBackgroundColor = 'rgba(13,142,4,0.3)'
    }

    // 设置为被预选中的样式
    #preSelect(element) {
        element.style.backgroundColor = 'rgba(255,0,77,0.3)';
        element.style.border = `3px dashed red`
    }

    // 设置为被选中的样式
    #select(element) {
        element.style.backgroundColor = 'rgba(13,142,4,0.3)';
        element.style.border = `3px solid green`
    }

    // 设置为原始样式
    #cancelSelect(element, styles) {
        element.style.backgroundColor = styles[0] || ''
        element.style.border = styles[1] || ''
    }

    // 将所有预选中元素转换成选中状态
    preSelect2Selected() {
        this.#preSelectElements.forEach((v, k, m) => {
            this.#selectedElements.set(k, v)
            k.style.backgroundColor = 'rgba(13,142,4,0.3)';
            k.style.border = `3px solid green`
        })
        this.#allowPreSelect = false
        // 保留匹配多个相似元素的xpath，删除用户选择的那一个元素的xpath
        this.#selectedXpathMatchUnique.splice(0, 1)
        this.#preSelectElements.clear()
    }

    clearPreSelect() {
        this.#preSelectElements.forEach((v, k, m) => {
            this.#cancelSelect(k, v)
        })
    }

    reset() {
        this.#preSelectElements.forEach((v, k, m) => {
            this.#cancelSelect(k, v)
        })
        this.#selectedElements.forEach((v, k, m) => {
            this.#cancelSelect(k, v)
        })
        this.#clearXpathTestSelect()
        this.#overElement = null
        this.#overElementOriginBackgroundColor = null
        this.#selectedElements = new Map()
        this.#preSelectElements = new Map()
        this.#selectedXpathMatchUnique = []
        this.#selectedXpathMatchSimilar = null
        this.#xpathTemplate = []
        this.#allowPreSelect = true
        this.#testXpathSelectElements = new Map()
        this.#selectScene = 0
    }

    getAllowPreSelect() {
        return this.#allowPreSelect
    }

    setAllowPreSelect(allowPreSelect) {
        this.#allowPreSelect = allowPreSelect
    }

    getSelectedElements() {
        return this.#selectedElements
    }

    getPreSelectElements() {
        return this.#preSelectElements
    }

    // 获取所有选中元素的xpath
    getSelectedXpath() {
        if (this.#preSelectElements.size > 0) {
            return this.#selectedXpathMatchUnique
        }
        if (this.#selectedXpathMatchSimilar === null) {
            return this.#selectedXpathMatchUnique
        }
        return [this.#selectedXpathMatchSimilar].concat(this.#selectedXpathMatchUnique)
    }

    xpathTest(xpathList) {
        this.#clearXpathTestSelect()
        for (let xpath of xpathList) {
            const eles = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
            for (let i = 0; i < eles.snapshotLength; i++) {
                const ele = eles.snapshotItem(i)
                // 记录选中的元素
                this.#testXpathSelectElements.set(ele, ele.style.border)
                // 设置边框表示测试选中
                ele.style.border = `3px dashed rgba(0,73,255,0.3)`
            }
        }
    }

    #clearXpathTestSelect() {
        this.#testXpathSelectElements.forEach((v, k, m) => {
            k.style.border = v
        })
        this.#testXpathSelectElements.clear()
    }

    extractData(xpathList, data, condition = null) {
        const dataList = []
        for (let xpath of xpathList) {
            const eles = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
            for (let i = 0; i < eles.snapshotLength; i++) {
                const ele = eles.snapshotItem(i)
                // 该数据提取节点在一个条件分支内，对于每一个要提取数据的元素项都要先检验其是否满足条件
                if (condition != null) {
                    switch (condition.type) {
                        case 0:
                            const regexp = new RegExp(condition.pattern)
                            if (!regexp.test(ele.innerText)) {
                                continue
                            }
                            break
                    }
                }
                const item = []
                for (let col of data) {
                    switch (col.type) {
                        case 0:
                            item.push(ele.innerText)
                            break
                        case 1:
                            let e = ele.parentElement.querySelector('a')
                            if (e != null) {
                                item.push(e.href)
                            } else {
                                e = ele.parentElement.querySelector('img')
                                if (e != null) {
                                    item.push(e.src)
                                }
                            }
                            break
                        default:
                            item.push(getExtraData(col.type))
                            break
                    }
                }
                dataList.push(item)
            }
        }
        console.log('dataType', dataList)
        return dataList
    }

    setSelectScene(scene) {
        this.#selectScene = scene
    }

    getSelectScene() {
        return this.#selectScene
    }
}

const elementSelector = new ElementSelector()

document.addEventListener('mouseover', (event) => elementSelector.mouseOver(event))
document.addEventListener('mouseout', (event) => elementSelector.mouseOut(event))
document.addEventListener('contextmenu', (event) => {
    elementSelector.mouseSelect(event)
    if (elementSelector.getSelectScene() === 1) {
        elementSelector.setSelectScene(0)
        return
    }
    console.log(elementSelector)

    // 每次右击选中与元素后，向操作渲染进程汇报最新的选中状态
    window.embeddedPageEvent.selectStatus(elementSelector.getAllowPreSelect(),
        elementSelector.getSelectedElements().size, elementSelector.getPreSelectElements().size, elementSelector.getSelectedXpath())
})

window.embeddedPageEvent.onCancelSelect((event) => {
    elementSelector.reset()
})

window.embeddedPageEvent.onSelectAll((event) => {
    elementSelector.preSelect2Selected()
    // 告知操作渲染进程最新的选择状况
    console.log(elementSelector.getSelectedXpath())
    window.embeddedPageEvent.selectStatus(elementSelector.getAllowPreSelect(),
        elementSelector.getSelectedElements().size, elementSelector.getPreSelectElements().size, elementSelector.getSelectedXpath())
})

window.embeddedPageEvent.onTestXpath((event, xpathList) => {
    elementSelector.reset()
    elementSelector.xpathTest(xpathList)
})

window.embeddedPageEvent.onPreviewData((event, xpathList, data, condition = null) => {
    const dataList = elementSelector.extractData(xpathList, data, condition)
    window.embeddedPageEvent.returnExtractData(dataList)
})

window.embeddedPageEvent.onSelectScene((event, selectScene) => {
    elementSelector.setSelectScene(selectScene)
})

window.embeddedPageEvent.onNeedExtraData((event, type) => {
    window.embeddedPageEvent.returnExtraData(type, getExtraData(type))
})

// 获取一些额外信息，主要是页面元信息
function getExtraData(type) {
    switch (type) {
        case 50:
            return window.location.href
        case 51:
            const titles = []
            document.querySelectorAll('title').forEach((e) => {
                titles.push(e.innerText)
            })
            return titles.join('')
        case 52:
            return currentDateTime()
    }
}


function currentDateTime() {
    const date = new Date();
    const year = date.getFullYear();
    const month = (date.getMonth() + 1).toString().padStart(2, '0');
    const day = date.getDate().toString().padStart(2, '0');
    const hour = date.getHours().toString().padStart(2, '0');
    const minute = date.getMinutes().toString().padStart(2, '0');
    const second = date.getSeconds().toString().padStart(2, '0');
    return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}